Connect Your React Application to a Rails API Using Active Storage (part 2)

Jennifer Ingram
10 min readSep 6, 2019

In my last post we started to build out a simple React application that used a Rails API with Active Storage on the backend to store user information (including an image file for their avatar). We created a user in our seed file, attached an image file to that user (in the form of a blob), and we were able to successfully fetch that user’s information from the backend and display their avatar on the frontend.

In this post (part 2), we’ll continue to build out our application, giving a user the ability to create an account from our frontend — including the ability to upload an avatar image from their computer — and we’ll store that information on the backend. If you missed part 1 of this post you can find it here.

Letting the User Logout

Before we move on to uploading an image, let’s give our users the ability to logout. To do that, I’m first going to refactor my NavBar Component. If a user is already logged in, they shouldn’t see the options to login or create an account on the NavBar — they should see a logout option instead.

Notice that now our NavBar is expecting some props. Let’s be sure to pass those down from our App Component.

Now we’re passing down state for our CurrentUser, and passing down a logout() function that will reset state to be null when a user logs out. Let’s also change our conditional for the profile route to redirect a user to the login page if they’re not already logged in, rather than re-rendering the Login Component. In order to do this we need to add the Redirect Component to the list of things that we’re importing from ‘react-router-dom’ at the top of our file.

import { Route, Switch, Redirect } from 'react-router-dom';

This makes our code a little cleaner, and little less breakable.

Sending Images to the Backend

Let’s move on now to our CreateAccount Component and build out that form.

This form should look very familiar — it’s almost the same as our login form, but with some minor (but very important) differences. First, notice that now we have an additional input field that has a type of ‘file’. This simple little change is what gives you a button, ‘Choose File’, in your form. When a user clicks on that button, a window automatically pops up (similar to a Finder window) and allows the user to choose a file stored on their computer.

All three input fields are using the handleOnChange() function that is setting state based on the value of each input field. Notice however, that when we fill out that form, state for the avatar is a weird ‘fakepath’ string. This isn’t what we need — what we really need is the file itself.

Let’s go back to our handleOnChange() function and do a bit of refactoring…

Because what we want from our file input is not the value (which is a simple string), but the file itself, we can refactor our function to first check if our input field has a name of ‘avatar’ — if it does, then we want the file that our user uploaded. We can grab that file using

event.target.files[0]

If the input field has a name of ‘name’ or ‘password’, then we will grab the values from those fields like before.

If we’ve grabbed our file correctly, then we should see it reflected in state…

Cool. Now this.state.avatar is a file object with a name, size, and type!

Let’s also give our form an onSubmit event listener…

In order to save our user’s information to the backend, we’ll separate this into a couple of steps. First, we’ll make a post fetch to ‘/users’ to create a new user, and then we’ll write a separate method to handle the image upload.

As of now, we’re just logging our information to the console — we’ll address what we’re going to do with the data we get back from our fetch call in a moment. Let’s turn to our backend first and fill out our create action in our users controller…

Now, when we create a new user we see his information logged to the console.

Once our User instance has been successfully saved on the backend, we can now write out the logic to upload his avatar. We’ll do this by writing a function, uploadFile(), that will take in two arguments — the file we want to upload, and the user object that we want to attach it to. We’ll invoke this function as soon as we get our user information back from the server.

Now this is where the magic of our activestorage library from node package manager comes in. At the top of our CreateAccount.js file we need to import a Component, DirectUpload, from this library…

import { DirectUpload } from 'activestorage';

Then we’ll create a new instance of that component in our uploadFile() function, passing it two arguments — the file we want to upload, and a url.

If we check out our routes on the backend, we can see that this url is indeed a valid path — it was provided for us when we installed Active Storage in our Rails application.

Even though this route does exist, we need to explicitly add it to our routes in our config folder on the backend. *Remember to kill your backend server and then start it up again to pick up those changes.

Now let’s attempt to create that upload. The create method provided by Active Storage takes in a callback function. That callback function takes in two arguments — an error, and a blob. If our create method is unsuccessful, let’s log the error, else, let’s log that there is no error.

Notice too, that this route goes to a create action, inside of a direct_uploads controller, that comes from Active Storage. A quick glance at your controllers on your backend will reveal that this controller does not exist (at least not anywhere that we can find it). Let’s build it out!

Notice here that our new DirectUploadsController inherits from ActiveStorage::DirectUploadsController. Let’s fill out our form again to create a new account and see what happens…

Ugh… we get this super ugly and mysterious error message — 422, Unprocessable Entity. We didn’t hit our byebug on the backend, but we can see that our create() function from the frontend did indeed console.log our error.

The problem here concerns Cross-Site Request Forgery. From the Ruby on Rails documentation:

Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks by including a token in the rendered HTML for your application. This token is stored as a random string in the session, to which an attacker does not have access. When a request reaches your application, Rails verifies the received token with the token in the session. All requests are checked except GET requests as these should be idempotent. Keep in mind that all session-oriented requests are CSRF protected by default, including JavaScript and HTML requests.

Well, that’s fine and dandy, but how do we get around it? Let’s place a skip_before_action helper method in our DirectUploadsController…

skip_before_action :verify_authenticity_token

*Disclaimer — I am fairly new to programming, and this was the only method I found to get activestorage to work with my ‘toy’ application. If you are building out a ‘real’ application (i.e. an application that real people will actually use on the real web) you don’t want to simply skip over any methods that are designed to prevent CSRF issues!

Now that we’ve added that line of code, when we submit our form we should be paused in our byebug from the create action in our terminal.

Looking closely at our parameters, we see that we have an object, a blob, with all of our image file’s information. We’ll use that to create a Blob instance (with a method provided by Active Storage).

Resubmitting the form again, and checking the value of blob, we see that we do indeed have a Blob object with its own id, key, and all of the other stuff we need to keep track of. If we check out the database too, we see that we now have that blob instance stored on our active_storage_blobs table.

Cool! Now we need to send that newly created blob back to our frontend (in json format), where we’ll make another fetch call to the backend that will update our user, by attaching their avatar file to their user instance. Phew!

Using a private method, direct_upload_json, we parse our blob object into json format and then send that information back to the frontend. Notice that along with our object, we are also passing along a method, signed_id. We’ll use this method on our frontend in a moment. For now, I’ve placed a debugger in our uploadFile() function that will trigger if we don’t get an error back…

And now we can check the value of blob…

Let’s update our user now with our new blob instance, created fresh from the server, by making another fetch request.

Notice now that we’re using that method, signed_id. In order to attach a blob file to our user, Active Record needs a key to connect the two — that’s what signed_id does for us. In our backend, we need to fill out the update action in our users controller.

Pausing in the byebug again, we can check our user instance after that user has been updated with their avatar. Remember however, that avatar is not an attribute of a user, rather an attached file. If we check the value of avatar_url we can see that one has been created.

Let’s also check out our database, just to be sure…

Our new user from above (with an id of 18) does indeed have an avatar attached! The blob_id of that avatar is 10 — so let’s check our blobs table to be sure that it’s in there…

And there’s blob 10! Awesome! We have successfully uploaded a file from our user, and attached that file to that user! Now we can go back to the frontend and use that information from our fetch call to set state for our CurrentUser and CurrentAvatar.

In our App Component we can write a function, updateCurrentUser() that will take the data we receive from that fetch call, and set state with our new information. We will then pass down that function, as props, to the CreateAccount Component. Last but not least, let’s change the last part of our fetch call to invoke that function with the data we get back…

Now create a user, and then click on the link to go to that users profile page…

Bam! Lookin’ good, Harrison.

--

--

Jennifer Ingram

Software Developer. Art and art history lover. Museum explorer. Total cat lady.